Запретить приложению создавать 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, выглядит так:
- Удалить
@UIApplication
атрибут из вашего класса делегата приложения Добавьте файл 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 см. Историю изменений.