Тестирование пользовательского интерфейса iOS на изолированном представлении

Я пытаюсь включить тесты пользовательского интерфейса в моем проекте iOS, но одна вещь, которая продолжает меня удерживать, - это тот факт, что все тесты, которые вы пишете, должны начинаться с самого начала приложения и прокладывать себе путь. Например, если я хочу протестировать представление, которое находится за экраном входа, мои тесты должны сначала запускаться на экране входа в систему, ввести имя пользователя/пароль, щелкнуть логин, а затем перейти к представлению, которое я хочу проверить. В идеале тесты для входа в систему и следующего будут полностью изолированы. Есть ли способ сделать это, или я полностью лишу философии тестов UI?

Ответ 1

Абсолютно!

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

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

Для этого вы можете создать объект только для тестирования, который реализует UIApplicationDelegate. Вы можете сказать, что приложение запускается в "режиме тестирования" и использует делегат приложения для тестирования, используя аргумент запуска.

Цель-C: main.m:

int main(int argc, char * argv[]) {
NSString * const kUITestingLaunchArgument   = @"org.quellish.UITestingEnabled";

    @autoreleasepool {
        if ([[NSUserDefaults standardUserDefaults] valueForKey:kUITestingLaunchArgument] != nil){
            return UIApplicationMain(argc, argv, nil, NSStringFromClass([TestingApplicationDelegate class]));
        } else {
            return UIApplicationMain(argc, argv, nil, NSStringFromClass([ProductionApplicationDelegate class]));
        }
    }
}

Swift: main.swift:

let kUITestingLaunchArgument = "org.quellish.UITestingEnabled"

if (NSUserDefaults.standardUserDefaults().valueForKey(kUITestingLaunchArgument) != nil){
    UIApplicationMain(Process.argc, Process.unsafeArgv, NSStringFromClass(UIApplication), NSStringFromClass(TestingApplicationDelegate))

} else {
    UIApplicationMain(Process.argc, Process.unsafeArgv, NSStringFromClass(UIApplication), NSStringFromClass(AppDelegate))
}

Вам придется удалить любую аннотацию @UIApplicationMain из ваших классов Swift.

Для "прикладных тестов" обязательно установите действие "Тест" схемы в Xcode, чтобы предоставить аргумент запуска:

Xcode Scheme editor

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

Objective-C:

XCUIApplication *app = [[XCUIApplication alloc] init];
[app setLaunchArguments:@[@"org.quellish.UITestingEnabled"] ];
[app launch];

Swift:

let app = XCUIApplication()
app.launchArguments = [ "org.quellish.UITestingEnabled" ]
app.launch()

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

Ответ 2

Если вы не против первоначальной загрузки пользовательского интерфейса, просто перейдите в целевой пользовательский интерфейс:

override func setUp() {
    super.setUp()
    continueAfterFailure = false
    XCUIApplication().launch()
    let storyboard = UIStoryboard(name: "MainStoryboard", bundle: NSBundle.mainBundle())
    let controller = storyboard.instantiateViewControllerWithIdentifier("LanguageSelectController")
    UIApplication.sharedApplication().keyWindow?.rootViewController = controller
}

Если вы не хотите, чтобы оригинальный пользовательский интерфейс находился под нагрузкой, а затем передайте это из теста:

app.launchArguments.append("skipEntryViewController")

а затем в didFinishLaunchingWithOptions вы можете проверить:

if NSProcessInfo.processInfo().arguments.contains("skipEntryViewController") {
    // then do NOT call makeKeyAndVisible
}

Ответ 3

К сожалению, при тестировании пользовательского интерфейса сценарий, который вы описываете, невозможен.

Один из подходов, который я принимаю для борьбы с этим, - это объединить мои тесты в "потоки" функций. Например, скажем, я хочу протестировать функцию A, функцию B и функцию C. Мне нужно войти в систему, чтобы все три работали.

Для каждого теста я не запускаю приложение, не вхожу в систему, а затем, наконец, запускаю фактический тест. Вместо этого я запускаю приложение и вхожу один раз. Затем я группирую свой тест на три частных вспомогательных метода: testFeatureA(), testFeatureB() и testFeatureC().

Создавая единый поток, набор тестов займет гораздо меньше времени. Большой недостаток заключается в том, что если функция A не работает, функция B никогда не будет проверена. Этот подход следует использовать только в том случае, если вам все равно, проходят ли все ваши тесты или нет.

Бонусные баллы за использование хелпера XCTest с параметрами __LINE__ и __FILE__ умолчанию. Затем вы можете передать их в свои XCTFail() чтобы отобразить строку сбоя на testFeatureA().