Измененный порядок загрузки + в Xcode 7
Я выяснил, что Xcode 7 (версия 7.0 (7A220)) изменил порядок, в котором методы +load
для классов и категорий вызываются во время модульных тестов.
Если категория, принадлежащая тестовой цели, реализует метод +load
, она теперь вызывается в конце, когда экземпляры класса могут быть уже созданы и использованы.
У меня есть AppDelegate
, который реализует метод +load
. Файл AppDelegate.m
также содержит категорию AppDelegate (MainModule)
. Кроме того, существует unit test файл LoadMethodTestTests.m
, который содержит другую категорию - AppDelegate (UnitTest)
.
Обе категории также реализуют метод +load
. Первая категория относится к основной цели, вторая - к тестовой цели.
Код
Я сделал небольшой тестовый проект, чтобы продемонстрировать эту проблему.
Это пустой проект по умолчанию для Xcode один, только с двумя измененными файлами.
AppDelegate.m:
#import "AppDelegate.h"
@implementation AppDelegate
+(void)load {
NSLog(@"Class load");
}
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
NSLog(@"didFinishLaunchingWithOptions");
return YES;
}
@end
@interface AppDelegate (MainModule)
@end
@implementation AppDelegate (MainModule)
+(void)load {
NSLog(@"Main Module +load");
}
@end
И файл unit test (LoadMethodTestTests.m):
#import <UIKit/UIKit.h>
#import <XCTest/XCTest.h>
#import "AppDelegate.h"
@interface LoadMethodTestTests : XCTestCase
@end
@interface AppDelegate (UnitTest)
@end
@implementation AppDelegate (UnitTest)
+(void)load {
NSLog(@"Unit Test +load");
}
@end
@implementation LoadMethodTestTests
-(void)testEmptyTest {
XCTAssert(YES);
}
@end
Тестирование
Я выполнил Unit Testing этого проекта (код и ссылка github ниже) на Xcode 6/7 и получил следующий порядок вызовов +load
:
Xcode 6 (iOS 8.4 simulator):
Unit Test +load
Class load
Main Module +load
didFinishLaunchingWithOptions
Xcode 7 (iOS 9 simulator):
Class load
Main Module +load
didFinishLaunchingWithOptions
Unit Test +load
Xcode 7 (iOS 8.4 simulator):
Class load
Main Module +load
didFinishLaunchingWithOptions
Unit Test +load
Вопрос
Xcode 7 запускает метод целевой категории +load
(Unit Test +load
) в конце, после того, как AppDelegate
уже создан.
Правильно ли это, или это ошибка, которую следует отправить Apple?
Может быть, он не указан, поэтому компилятор/среда выполнения могут свободно переупорядочивать звонки?
Я рассмотрел этот вопрос SO, а также описание + load в документации NSObject, но я не совсем понял, как должен работать метод +load
, когда категория принадлежит другой цели.
Или может быть AppDelegate
по какой-то причине является своего рода частным случаем?
Почему я спрашиваю об этом
- Образовательные цели.
- Я использовал метод swizzling в категории внутри цели unit test. Теперь, когда порядок вызова изменился,
applicationDidFinishLaunchingWithOptions
выполняется до того, как происходит swizzling. По-моему, есть другие способы сделать это, но для Xcode 7. мне кажется, что он интуитивно понятен, так как он работает в Xcode 7. Я думал, что когда класс загружается в память, +load
этого класса и +load
методы всех его категорий должны быть вызваны, прежде чем мы сможем что-то с этим классом (например, создать экземпляр и вызвать didFinishLaunching...
).
Ответы
Ответ 1
TL, DR: Это xctest ошибка, а не objc.
Это связано с тем, как исполняемый файл xctest
(тот, который фактически запускает модульные тесты, расположенные в $XCODE_DIR/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/Library/Xcode/Agents/xctest
, загружает свой пакет.
Pre-Xcode 7, он загрузил пакеты all, указанные в тестовых пакетах, перед запуском любых тестов. Это можно увидеть (для тех, кто заботится), разобрав двоичный код для Xcode 6.4, соответствующий раздел можно увидеть для символа -[XCTestTool runTestFromBundle:]
.
В версии Xcode 7 xctest
вы можете видеть, что она задерживает загрузку тестовых пакетов до тех пор, пока фактический тест не будет выполняться XCTestSuite
, в фактической структуре xctest
, которую можно увидеть в символе __XCTestMain
, который вызывается только после установки тестового хост-приложения.
Поскольку порядок их вызывается внутренне изменен, способ, которым вы вызываете методы test +load
, отличается. Не было внесено никаких изменений в внутренние функции objective-c -runtime.
Если вы хотите исправить это в своем приложении, вы можете сделать несколько вещей. Сначала вы можете вручную загрузить свой пакет с помощью +[NSBundle bundleWithPath:]
и вызвать -load
.
Вы также можете связать свою тестовую цель с тестовым хост-приложением (я надеюсь, что вы используете отдельный тестовый хост, чем основное приложение!), что сделает его автоматически загруженным, когда xctest загрузит хост-приложение.
Я бы не счел это ошибкой, это просто деталь реализации XCTest.
Источник: просто потратьте последние 3 дня на демонтаж xctest
по совершенно не связанной причине.
Ответ 2
Xcode 7 имеет два разных порядка загрузки в проекте шаблона iOS.
Unit Test Case. Для Unit Test тестовый комплект вводится в текущую симуляцию после запуска приложения на главный экран. По умолчанию последовательность выполнения Unit Test выглядит следующим образом:
Application: AppDelegate initialize()
Application: AppDelegate init()
Application: AppDelegate application(…didFinishLaunchingWithOptions…)
Application: ViewController viewDidLoad()
Application: ViewController viewWillAppear()
Application: AppDelegate applicationDidBecomeActive(…)
Application: ViewController viewDidAppear()
Unit Test: setup()
Unit Test: testExample()
Тест пользовательского интерфейса.. Для тестирования пользовательского интерфейса создается отдельный второй процесс XCTRunner
, который выполняет тестируемое приложение. Аргумент может быть передан из теста setUp()
...
class Launch_UITests: XCTestCase {
override func setUp() {
// … other code …
let app = XCUIApplication()
app.launchArguments = ["UI_TESTING_MODE"]
app.launch()
// … other code …
}
... для приема будет AppDelegate
...
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(… didFinishLaunchingWithOptions… ) -> Bool {
// … other code …
let args = NSProcessInfo.processInfo().arguments
if args.contains("UI_TESTING_MODE") {
print("FOUND: UI_TESTING_MODE")
}
// … other code …
Впрыскивание в отдельный процесс можно наблюдать путем печати NSProcessInfo.processInfo().processIdentifier
и NSProcessInfo.processInfo().processName
как из тестового кода, так и из кода приложения.