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.

    • +18
    • 4,8k
    • 3
    FunCorp 36,81
    Компания
    Поделиться публикацией
    Похожие публикации
    Комментарии 3
    • 0
      Спасибо за статью, интересно. Хотел бы спросить, пользуясь случаем, а название «АйДаПрикол» почему было выбрано? Вот iFunny — понятно, современно, модно, молодежно, а это? Понятно, адаптированно для российского рынка и все дела, но лично у меня в свете того же iFunny возникают ассоциации с каким-то обывательством и Одноклассниками, так и кажется что там поздравления с Пасхой и Троицей и «Лайк бабушке-рукодельнице» будут вылезать…

      И да, если производитель заявлен FunCorp, то что означает Okrujnost' на Маркете? А так, молодцы, я вообще не ожидал, что компания, разрабатывающая три несложных приложения (сам пишу менеджер приложений в одиночку, знаю что это за процесс), располагается на Кипре, имеет штат сотрудников, офисы в разных частях мира и даже вакансии. Если честно, не думал, что это настолько прибыльно, думал что это чистый альтруизм и что только топовые приложения от топовых компаний так умеют…
      • +1
        Приложения создавались 6 лет назад, в эпоху первых Айфонов/iPhone, поэтому и АйДаПрикол/iFunny вышли вариациями на тему. К слову, АйДаПрикол был первее :)
        • 0
          Вот но что! Это многое объясняет, благодарю)

      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

      Самое читаемое