10

当我使用 OCUnit 测试我的应用程序时,它会在运行测试之前像往常一样设置 AppDelegate、窗口和 rootViewController。我的 rootViewController 然后将自己添加为某些 NSNotifications 的观察者。

当我用隔离的测试实例和模拟观察者测试这些通知时,自动创建的 rootViewController 的通知处理程序也会被调用,这会导致我的一些测试失败。

在测试模式下运行时,有没有办法阻止 OCUnit 创建 rootViewController 或使其使用不同的 ViewController 类?如果不用在我的应用程序代码中编写特殊的测试相关代码就可以做到这一点,那就太酷了。

4

4 回答 4

12

更新:我今天所做的与下面的答案略有不同。了解如何轻松切换您的 App Delegate 以进行测试

它确实需要在您的应用程序代码中添加一些特定于测试的代码。以下是我为避免完整的启动顺序而采取的措施:

编辑方案

  • 选择测试操作
  • 在“测试”中选择参数选项卡
  • 禁用“使用运行操作的选项”
  • 添加环境变量,设置runningTestsYES

编辑您的应用委托

  • -application:didFinishLaunchingWithOptions:在有意义的情况下添加以下内容:

    #if DEBUG
        if (getenv("runningTests"))
            return YES;
    #endif
    
  • 做同样的事情,-applicationDidBecomeActive:但很简单return

于 2012-08-16T05:31:49.357 回答
3

@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://stackoverflow.com/questions/11974138/prevent-app-from-creating-a-viewcontroller-when-running-unit-tests/11981192#11981192
}

@end

如果有人提出更好的解决方案,请告诉我:)

为什么我将其发布为答案:这只是对@Jon Reid 答案的一个小改进(我真的很喜欢)。我想把它写成评论,但是以这种方式共享代码会很不方便,所以我决定将它作为答案发布(尽管它并不完全是问题的答案)。

于 2015-06-04T09:47:52.423 回答
2

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:

在 swift 中,您需要将其包装在一个类中以便在 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"
    }()
}
于 2015-08-22T08:26:44.630 回答
1

我在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,请参阅编辑历史。

于 2016-09-13T09:51:09.770 回答