Запретить приложению создавать viewcontroller при запуске модульных тестов

Когда я тестирую свое приложение с помощью OCUnit, оно настраивает AppDelegate, window и rootViewController, как обычно, перед запуском тестов. Мой rootViewController затем добавляет себя в качестве наблюдателя для некоторых NSNotifications.

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

Есть ли способ не дать OCUnit создать rootViewController или заставить его использовать другой класс ViewController при работе в тестовом режиме? Было бы здорово, если бы это можно было сделать без написания специального кода, связанного с тестами, в коде моего приложения.

4 ответа

Решение

Обновление: то, что я делаю сегодня, немного отличается от ответа ниже. Посмотрите, как легко переключить ваш делегат приложения для тестирования

Это требует добавления небольшого количества специфичного для теста кода в код вашего приложения. Вот что я делаю, чтобы избежать полной последовательности запуска:

Редактировать схему

  • Выберите действие Тест
  • В "Тесте" выберите вкладку "Аргументы"
  • Отключить "Использовать параметры действия" Выполнить ""
  • Добавить переменную среды, настройку runningTests в YES

Изменить делегата приложения

  • Добавьте следующее к -application:didFinishLaunchingWithOptions: как только это имеет смысл:

    #if DEBUG
        if (getenv("runningTests"))
            return YES;
    #endif
    
  • Сделать то же самое для -applicationDidBecomeActive: но просто return,

Решение @Jon Reid великолепно, и сейчас я использую его во всех своих проектах, но есть небольшая проблема: схемы не сохраняются в системе контроля версий по умолчанию. Поэтому, когда вы клонируете проект из git, тесты могут провалиться только потому, что runningTests переменная окружения не установлена. И я все время об этом забываю.

Итак, чтобы напомнить себе об этом, теперь я добавляю небольшой тест ко всем моим проектам:

#import <UIKit/UIKit.h>
#import <XCTest/XCTest.h>

@interface DMAUnitTestModeTests : XCTestCase

@end

@implementation DMAUnitTestModeTests

- (void)testUnitTestMode {
    BOOL isInUnitTestMode = (BOOL)getenv("runningTests");

    XCTAssert(isInUnitTestMode, @"You have to set a 'runningTests' environment variable in the schemes editor.");
    //http://stackru.com/questions/11974138/prevent-app-from-creating-a-viewcontroller-when-running-unit-tests/11981192#11981192
}

@end

Если кто-то придумает лучшее решение, пожалуйста, дайте мне знать:)

Почему я опубликовал это как ответ: это лишь небольшое улучшение ответа @Jon Reid (что мне действительно нравится). Я хотел написать это как комментарий, но было бы неудобно делиться кодом таким образом, поэтому я решил опубликовать его как ответ (несмотря на то, что это не совсем ответ на вопрос).

Сам Xcode устанавливает переменные среды при запуске тестов, поэтому нет необходимости создавать какие-либо схемы в них. Если вы уже делаете это для других целей, то это может быть практичным. Однако вы можете использовать переменные среды XCode для определения того, выполняются ли тесты. Основная часть кода выглядит следующим образом в objc, который вы можете добавить в делегат приложения:

Опция 1:

static BOOL isRunningTests(void) __attribute__((const));

static BOOL isRunningTests(void)
{
    NSDictionary* environment = [[NSProcessInfo processInfo] environment];
    NSString* injectBundle = environment[@"XCInjectBundle"];
    NSLog(@"TSTL %@", [injectBundle pathExtension]);
    return [[injectBundle pathExtension] isEqualToString:@"xctest"] || [[injectBundle pathExtension] isEqualToString:@"octest"];
}

Тогда просто позвони isRunningTests() везде, где вам нужно проверить на тесты. Этот код, однако, должен действительно храниться где-то еще, например, в классе TestHelper:

Вариант 2:

// TestHelper.h
#import <Foundation/Foundation.h>

extern BOOL isRunningTests(void) __attribute__((const));
// TestHelper.m
#import "TestCase.h"

extern BOOL isRunningTests(void)
{
    NSDictionary* environment = [[NSProcessInfo processInfo] environment];
    NSString* injectBundle = environment[@"XCInjectBundle"];
    NSLog(@"TSTL %@", [injectBundle pathExtension]);
    return [[injectBundle pathExtension] isEqualToString:@"xctest"] || [[injectBundle pathExtension] isEqualToString:@"octest"];
}

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

Вариант 3:

И в быстром, вам нужно будет обернуть его в классе, чтобы работать как в Objective-C и Swift. Вы можете сделать это так:

class TestHelper: NSObject {
    static let isRunningTests: Bool = {
        guard let injectBundle = NSProcessInfo.processInfo().environment["XCInjectBundle"] as NSString? else {
            return false
        }
        let pathExtension = injectBundle.pathExtension

        return pathExtension == "xctest" || pathExtension == "octest"
    }()
}

Самый чистый способ, который я видел в примере приложения RxTodo MVVM, выглядит так:

  1. Удалить @UIApplication атрибут из вашего класса делегата приложения
  2. Добавьте файл main.swift с такой реализацией:

    import UIKit
    import Foundation
    
    final class MockAppDelegate: UIResponder, UIApplicationDelegate {}
    
    private func appDelegateClassName() -> String {
        let isTesting = NSClassFromString("XCTestCase") != nil
        return
        NSStringFromClass(isTesting ? MockAppDelegate.self : AppDelegate.self)
    }
    
    UIApplicationMain(
        CommandLine.argc,
        UnsafeMutableRawPointer(CommandLine.unsafeArgv)
            .bindMemory(to: UnsafeMutablePointer<Int8>.self, capacity: Int(CommandLine.argc)),
        NSStringFromClass(UIApplication.self), appDelegateClassName()
    )
    

Это версия Swift 3. Для v2 см. Историю изменений.

Другие вопросы по тегам