Как мы тестируем Сбербанк Онлайн на iOS

в 14:00, , рубрики: Блог компании Сбербанк, сбербанк онлайн, Тестирование IT-систем, Тестирование мобильных приложений
Как мы тестируем Сбербанк Онлайн на iOS - 1

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

Ситуация такова: предположим, разработчикам удалось убедить себя и бизнес в необходимости покрытия кодовой базы тестами. Со временем в проекте стало более десятка тысяч unit- и более тысячи UI-тестов. Такая большая тестовая база породила несколько проблем, о решении которых мы хотим рассказать.

В первой части статьи мы ознакомимся с трудностями, возникающими при работе с чистыми (не интеграционными) unit-тестами, во второй части будут рассмотрены UI-тесты. Чтобы узнать, как мы улучшаем стабильность тестовых прогонов, добро пожаловать под кат.

В идеальном мире при неизменяемом исходном коде unit-тесты должны всегда показывать один и тот же результат независимо от количества и последовательности запусков. А постоянно падающие тесты не должны проходить через барьер Continuous Integration server (CI).

Как мы тестируем Сбербанк Онлайн на iOS - 2

В действительности же можно столкнуться с тем, что один и тот же unit-тест будет показывать то положительный, то отрицательный результат — а значит «мигать». Причина такого поведения кроется в плохой реализации кода теста. Причем такой тест может пройти CI при удачном прогоне, а позже он начнет падать на чужих Pull Request (PR). В подобной ситуации возникает желание отключить этот тест или сыграть в рулетку и запустить прогон CI повторно. Однако такой подход анти-продуктивный, так как подрывает доверие к тестам и загружает CI бессмысленной работой.

Данная проблематика была освещена в этом году на международной конференции WWDC компании Apple:
— В этой сессии рассказывается про параллельное тестирование, анализ покрытия кода отдельного таргета тестами, а также про порядок запуска тестов.
Тут Apple рассказала про тестирование сетевых запросов, мокирование, тестирование нотификаций и скорость выполнения тестов.

Unit tests

Для борьбы с мигающими тестами воспользуемся следующей последовательностью действий:
image
0. Оцениваем код теста на качество по базовым критериям: изолированность, корректность моков и т.д. Соблюдаем правило: при мигающем тесте меняем код теста, а не тестируемый код. Если данный пункт не помог, то далее действуем так:
1. Фиксируем и воспроизводим условия, при которых тест падает;
2. Находим причину, по которой произошло падение;
3. Меняем код теста или тестируемый код;
4. Переходим к первому шагу и проверяем, устранена ли причина падения.

Воспроизводим падение

Самый простой и очевидный вариант — запустить проблемный тест на той же версии iOS и на том же устройстве. Как правило, в этом случае тест выполняется успешно, и появляется мысль: «У меня локально все работает, перезапущу сборку на CI”. Вот только на самом деле проблема не была решена, и тест продолжает падать у кого-то другого.

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

Если вся последовательность тестов выполнилась успешно и не удалось зафиксировать ожидаемое падение, можно повторить прогон значительное количество раз.
Для этого в командной строке нужно запустить цикл с xcodebuild:

#! /bin/sh
x=0
while [ $x -le 100 ];
    do xcodebuild -configuration Debug -scheme "TargetScheme" -workspace App.wcworkspace -sdk iphonesimulator -destination "platfrom=iOS Simulator, OS=11.3, name=iPhone 7" test >> "report.txt";
    x=$(( $x +1 ));
done

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

Причины падения и возможные решения

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

Можно выделить три основные группы причин падения тестов:

Слабая изоляция
Под изоляцией мы понимаем частный случай инкапсуляции, а именно: механизм языка, позволяющий ограничить доступ одних компонентов программы к другим.
Изолированность среды играет важную роль, так как для чистоты проверки ничто не должно воздействовать на тестируемые сущности. Особое внимание стоит уделить тестам, которые нацелены на проверку кода. В них используются сущности с глобальным состоянием, такие как: глобальные переменные, Keychain, Network, CoreData, Singleton, NSUserDefaults и так далее. Именно в этих областях возникает наибольшее количество потенциальных мест для проявления слабой изоляции. Допустим, при создании окружения теста задается глобальное состояние, которое неявно используется в другом тестируемом коде. В этом случае тест, проверяющий тестируемый код, может начать “мигать”. Потому что в зависимости от последовательности тестов может возникнуть две ситуации — когда глобальное состояние задано и когда не задано. Зачастую описанные зависимости являются неявными, поэтому можно случайно забыть установить/сбросить подобные глобальные состояния.

Чтобы зависимости были явно видны, можно воспользоваться принципом Dependency Injection (DI), а именно: передавать зависимость через параметры конструктора, либо свойство объекта. Это позволит легко подставить мок-зависимости вместо реального объекта.

Асинхронность вызовов
Все unit-тесты выполняются синхронно. Сложность тестирования асинхронности возникает по причине того, что вызов тестируемого метода в тесте “застывает” в ожидании завершения выполнения скоупа unit-теста. Результатом будет стабильное падение теста.


	//act
	[self.testService loadImageFromUrl:@"www.google.ru" handler:^(UIImage * _Nullable image, NSError * _Nullable error) {
		//assert
		OCMVerify([cacheMock imageAtPath:OCMOCK_ANY]);
		OCMVerify([cacheMock dateOfFileAtPath:OCMOCK_ANY]);
		OCMVerify([imageMock new]);
		[imageMock stopMocking];
	}];
	
	[self waitInterval:0.2];

Чтобы проверить такой тест, существует несколько подходов:
1) Прогон NSRunLoop
2) waitForExpectationsWithTimeout

Оба варианта требуют указать аргумент со временем ожидания. Однако нельзя гарантировать, что выбранного интервала будет достаточно. Локально ваш тест пройдет, но на высоко нагруженном CI может не хватить мощностей и он упадет — отсюда появится “мигание”.

Пусть у нас есть некий сервис обработки данных. Мы хотим проверить, что после получения ответа от сервера он передает эти данные на обработку дальше.
Для отправки запросов по сети сервис использует клиента для работы с ней.

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


@interface Service : NSObject

@property (nonatomic, strong) id<APIClient> apiClient;

@end

@protocol APIClient <NSObject>

- (void)getDataWithCompletion:(void (^)(id responseJSONData))completion;

@end

- (void)testRequestAsync
{
  // arrange
    __auto_type service = [Service new];
    service.apiClient = [APIClient new];

    XCTestExpectation *expectation = [self expectationWithDescription:@"Request"];

    // act
    id receivedData = nil;
    [self.service receiveDataWithCompletion:^(id responseJSONData) {
        receivedData = responseJSONData;
        [expectation fulfill];
    }];

    [self waitForExpectationsWithTimeout:10 handler:^(NSError * _Nullable error) {
        expect(receivedData).notTo.beNil();
        expect(error).to.beNil();
    }];
}

Но синхронный вариант теста будет более стабильным и позволит избавиться от работы с таймаутами.
Для него нам нужен синхронный мок APIClient


@interface APIClientMock : NSObject <APIClient>
@end

@implementation

- (void)getDataWithCompletion:(void (^)(id responseJSONData))completion
{
  __auto_type fakeData = @{ @"key" : @"value" };
  if (completion != nil)
  {
    completion(fakeData);
  }
}

@end

Тогда тест будет выглядеть проще, а работать стабильнее


- (void)testRequestSync
{
  // arrange
    __auto_type service = [Service new];
    service.apiClient = [APIClientMock new];

    // act
    id receivedData = nil;
    [self.service receiveDataWithCompletion:^(id responseJSONData) {
        receivedData = responseJSONData;
    }];

    expect(receivedData).notTo.beNil();
    expect(error).to.beNil();
}

Работу с асинхронностью можно изолировать путем инкапсулирования в отдельную сущность, которую можно протестировать независимо. Остальную логику необходимо тестировать синхронно. Данный подход позволит избежать большинство подводных камней, привносимых асинхронностью.

Как вариант, в случае с обновлением UI-слоя из background-потока, можно сделать проверку на то, находимся ли мы в главном потоке, и что будет происходить, если мы делаем вызов из теста:


func performUIUpdate(using closure: @escaping () -> Void) {
    // If we are already on the main thread, execute the closure directly
    if Thread.isMainThread {
        closure()
    } else {
        DispatchQueue.main.async(execute: closure)
    }
}

Развернутое объяснение смотрите в статье Д.Санделла.

Тестирование кода, выходящего за пределы вашего контроля
Часто мы забываем о следующих вещах:
реализация методов может зависеть от локализации применения,
— в SDK есть приватные методы, которые могут вызываться классами фреймворка,
— реализация методов может зависеть от версии SDK

Как мы тестируем Сбербанк Онлайн на iOS - 4

Как мы тестируем Сбербанк Онлайн на iOS - 5

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

На этом мы хотим завершить первую часть статьи об автоматизированном тестировании iOS-приложения Сбербанк Онлайн, посвященную unit-тестированию.
Во второй части статьи мы расскажем о проблемах, возникших при написании 1500 UI-тестов, а также рецептах их преодоления.

Статью писали вместе с regno — Антон Власов, руководитель направления и iOS developer.

Автор: victoriaqb

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js