在运行单元测试时阻止应用程序创建viewcontroller

时间:2012-08-15 17:31:05

标签: ios unit-testing uiviewcontroller tdd ocunit

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

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

有没有办法让OCUnit在测试模式下运行时不创建rootViewController或让它使用不同的ViewController类?如果可以在我的应用程序代码中没有编写与测试相关的特殊代码的情况下完成,那将会很酷。

4 个答案:

答案 0 :(得分:10)

更新:我今天所做的与下面的答案略有不同。请参阅How to Easily Switch Your App Delegate for Testing

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

编辑方案

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

修改您的应用代理

  • 一旦有意义,就将以下内容添加到-application:didFinishLaunchingWithOptions:

    #if DEBUG
        if (getenv("runningTests"))
            return YES;
    #endif
    
  • -applicationDidBecomeActive:执行相同操作,但只需return

答案 1 :(得分:1)

@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的答案(我非常喜欢)的一个小改进。我想把它写成评论,但以这种方式共享代码会很不方便,所以我决定将其作为答案发布(尽管事实上它并不是问题的答案)。

答案 2 :(得分:1)

Xcode本身在运行测试时设置环境变量,因此无需在您的方案中创建任何环境变量。如果您已经出于其他目的这样做,那么这样做可能是实际的。但是,您可以使用Xcode的环境变量来确定测试是否正在运行。大部分代码在objc中都是这样的,你可以把它扔进你的app delegate:

选项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"
    }()
}

答案 3 :(得分:1)

我在RxTodo MVVM example app中看到的最干净的方式,就是这样:

  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()
    )
    
  3. 这是Swift 3版本。对于v2,请参阅编辑历史记录。