Изменен + порядок загрузки метода в Xcode 7

Я узнал, что Xcode 7 (версия 7.0 (7A220)) изменил порядок, в котором +load методы для классов и категорий вызываются во время юнит-тестов.

Если категория, принадлежащая цели теста, реализует +load метод, теперь он вызывается в конце, когда экземпляры класса, возможно, уже были созданы и использованы.

у меня есть AppDelegate, который реализует +load метод. AppDelegate.m файл также содержит AppDelegate (MainModule) категория. Кроме того, есть файл модульного теста LoadMethodTestTests.m, который содержит другую категорию - AppDelegate (UnitTest),

Обе категории также реализуют +load метод. Первая категория относится к основной цели, вторая - к контрольной цели.

Код

Я сделал небольшой тестовый проект, чтобы продемонстрировать проблему. Это пустой проект по умолчанию Xcode one view с измененными только двумя файлами.

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

И файл модульного теста (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

тестирование

Я выполнил модульное тестирование этого проекта (код и ссылка на 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 это какой-то особый случай по какой-то причине?

Почему я спрашиваю это

  1. Образовательные цели.
  2. Я использовал метод swizzling в категории внутри цели модульного теста. Теперь, когда порядок звонков изменился, applicationDidFinishLaunchingWithOptions выполняется до того, как произойдет бурение. Я верю, что есть и другие способы сделать это, но мне просто кажется нелогичным, как это работает в Xcode 7. Я подумал, что когда класс загружается в память, +load этого класса и +load методы всех его категорий должны вызываться до того, как мы сможем что-то с этим классом (например, создать экземпляр и вызвать didFinishLaunching...).

2 ответа

Решение

TL,DR: это вина xctest, а не цель.

Это из-за того, как xctest исполняемый файл (тот, который фактически запускает модульные тесты, расположенный в $XCODE_DIR/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/Library/Xcode/Agents/xctest загружает свою связку.

До Xcode 7 он загружал все упомянутые тестовые пакеты перед запуском любых тестов. Это можно увидеть (для тех, кто заботится), разобрав двоичный файл для Xcode 6.4, можно увидеть соответствующий раздел для символа -[XCTestTool runTestFromBundle:],

В версии Xcode 7 xctestВы можете видеть, что это задерживает загрузку тестовых пакетов до тех пор, пока XCTestSuiteв фактическом XCTest рамки, которые можно увидеть в символе __XCTestMain, который вызывается только ПОСЛЕ настройки хост-приложения теста.

Поскольку порядок их вызова внутренне изменился, способ, которым ваш тест +load Методы вызываются иначе. Не было никаких изменений, внесенных во внутреннюю часть target-c-runtime.

Если вы хотите исправить это в своем приложении, вы можете сделать несколько вещей. Во-первых, вы можете вручную загрузить свой пакет, используя +[NSBundle bundleWithPath:]и вызывая -load на что.

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

Я бы не стал считать это ошибкой, это просто деталь реализации XCTest.

Источник: просто потратить последние 3 дня на разборку xctest по совершенно не связанной причине.

Xcode 7 имеет два разных порядка загрузки в проекте шаблона iOS.

Модульный тест Для модульного теста тестовый пакет вводится в работающую симуляцию после запуска приложения на главном экране. Последовательность выполнения модульного теста по умолчанию выглядит следующим образом:

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()

UI Test Case. Для UI Test, отдельный второй процесс 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 как из кода теста, так и из кода приложения.

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