在Xcode中进行单元测试,它会运行应用程序吗?

47

我遇到了一个奇怪的问题,之前从未遇到过。

当你按下cmd+U运行单元测试(例如OCUnit),它是否实际上调用main.m,新建appDelegate并像按下cmd+R一样运行应用程序?

我之所以问这个问题,是因为我正在DataLayer中使用CoreData。我在我的测试中成功模拟了DataLayer,但是一旦我实现了一个实际调用CoreData的getAll方法,应用程序/Xcode就会抛出关于托管对象模型不能为空的异常。我理解为什么会这样,但我并不想真正地新建DataLayer类,并且我已经在mainviewcontroller的loadView方法中放置了一个断点,它正在调用DataLayer的getAll方法。由于这是一个模拟对象,因此在测试中应该没有关系,但显然它正在调用真实实例。

那么回到我的问题,按下cmd+U后,它是否先运行应用程序再运行测试?

10个回答

64

这个应用程序实际上已经在运行,但是有一个小技巧可以防止它继续运行。

int main(int argc, char* argv[]) {
    int returnValue;

    @autoreleasepool {
        BOOL inTests = (NSClassFromString(@"SenTestCase") != nil
                     || NSClassFromString(@"XCTest") != nil);    

        if (inTests) {
            //use a special empty delegate when we are inside the tests
            returnValue = UIApplicationMain(argc, argv, nil, @"TestsAppDelegate");
        }
        else {
            //use the normal delegate 
            returnValue = UIApplicationMain(argc, argv, nil, @"AppDelegate");
        }
    }

    return returnValue;
}

1
非常好,我从未想过替换不同的应用程序委托!我会简化测试,只需使用“BOOL runningTests = NSClassFromString(@”SenTestCase“)!= nil;”。 - Jon Reid
@JonReid 不错的补充。我从未想过可以这样简化它! - Sulthan
2
对于XCODE 5 - BOOL inTests = (NSClassFromString(@"XCTest") != nil); - Pion
1
@CarlJ 这可能是您项目设置中的问题。请注意,OCTest/XCTest框架不应链接到您的主目标中。确保它只在您的测试目标中。 - Sulthan
1
如果我有逻辑测试、应用程序测试和正常的应用程序AppDelegates,我想在它们之间切换,该怎么办?如何在应用程序和逻辑测试之间切换? - fatuhoku
显示剩余3条评论

20

这是 Sulthan 的答案的一个变体,它使用 XCTest,这是由 XCode 5 生成的测试类的默认值。


int main(int argc, char * argv[])
{
    @autoreleasepool {
        BOOL runningTests = NSClassFromString(@"XCTestCase") != nil;
        if(!runningTests)
        {
            return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
        }
        else
        {
            return UIApplicationMain(argc, argv, nil, @"TestAppDelegate");
        }
    }
}

这段代码需要添加到 Supporting Files 文件夹下的 main.m 中,这是标准项目布局。

然后在您的测试目录中添加:

TestAppDelegate.h


#import <Foundation/Foundation.h>

@interface TestAppDelegate : NSObject<UIApplicationDelegate>
@end

TestAppDelegate.m


#import "TestAppDelegate.h"

@implementation TestAppDelegate
@end

13

在Swift中,我更喜欢在application: didFinishLaunchingWithOptions内部绕过正常的执行路径:

func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
    guard normalExecutionPath() else {
        window = nil
        return false
    }

    // regular setup

    return true
}

private func normalExecutionPath() -> Bool {
    return NSClassFromString("XCTestCase") == nil
}

guard 语句中的代码会删除从 storyboard 创建的任何视图。


这对我来说完美无缺,而且似乎是最简单的解决方案!谢谢。 - mikey

5
如果您正在使用Swift(您可能没有main.c),则需要执行以下步骤:
1:在AppDelegate.swift中删除@UIApplicationMain 2:创建一个空的TestingAppDelegate.swift文件
import UIKit
class TestingAppDelegate: UIResponder, UIApplicationDelegate {
    var window: UIWindow?
}

3:创建一个名为 main.swift 的文件:

import Foundation
import UIKit

let isRunningTests = NSClassFromString("XCTestCase") != nil

if isRunningTests {
   UIApplicationMain(C_ARGC, C_ARGV, nil, NSStringFromClass(TestingAppDelegate))
} else {
   UIApplicationMain(C_ARGC, C_ARGV, nil, NSStringFromClass(AppDelegate))
}

3
Xcode 7 (beta 5) + iOS9 - 使用Process.argc和Process.unsafeArgv代替C_ARGC和C_ARGV。 - stevo.mit
1
使用这种方法,测试AppDelegate是否仍会导致故事板加载?如果是这样,能否避免这种情况发生? - William Entriken
Full Decent - 是的,它仍然可以加载。有一种解决方法,但我想找到更好的方法。复制正在测试的目标,我们称之为myApp,复制品为myApp copy。取消myApp copy目标的Main.storyboard成员资格。添加一个名为Main.storyboard的新故事板,并将其设置为myApp copy目标的成员。更改myApp测试目标,使其测试myApp copy而不是myApp。现在将使用空的Main.storyboard,并且不会实例化任何视图控制器。但我仍希望能够找到更简单的方法。 - Rocket Garden

2
我找到了另一个解决问题的方案:
int main(int argc, char * argv[])
{
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, ({
            ![NSProcessInfo processInfo].environment[@"XCTestConfigurationFilePath"] ?
            @"AppDelegate" :
            nil;                
        }));
    }
}

从这里开始:http://qualitycoding.org/app-delegate-for-tests/#comment-63984 这篇文章讨论了在进行iOS应用程序测试时,如何使用AppDelegate。作者建议将测试代码放入AppDelegate扩展中,以便在测试期间更轻松地访问应用程序状态和属性。此外,他还提供了一些有关如何在测试期间设置应用程序委托的技巧。

2
你可以通过在测试目标中将主机应用程序设置为“无”来实现这一点。 enter image description here

2
使用xCode 7和xCtool
xCtool能够在不运行应用程序的情况下执行单元测试。
为了使其工作,
1. 更新目标设置以无需主机应用程序运行。
选择您的项目 -> 然后测试目标 -> 将主机应用程序设置为none。

enter image description here

安装xctool,如果您尚未安装。
brew install xctool

使用终端和xctool运行测试。
xctool -workspace yourWorkspace.xcworkspace -scheme yourScheme run-tests -sdk iphonesimulator

1
然而,这些测试不会在iOS应用程序的上下文中运行,许多功能将无法正常工作。例如钥匙串和核心数据。如果您有UI测试,这根本不起作用。但对于单元测试来说是可以的。 - Sulthan

1

是的,您的测试目标将有一个依赖于应用程序目标,因此当您按下Cmd+U或Cmd+Shift+U时,应用程序目标将会被构建。


1

优秀的 答案 表明可以在运行时动态更改应用程序委托。

我做出的小修改是通过查询 NSProcessInfo检测单元测试运行。优点是您不需要拥有一个可检测的类来查看是否正在运行单元测试。

    int main(int argc, char * argv[])
    {
        // Put your App delegate class here.
        const Class appDelegateClass = [ATAppDelegate class];

        NSDictionary *const environmentDictionary =
        [[NSProcessInfo processInfo] environment];

        const BOOL runningUnitTests =
        environmentDictionary[@"XCInjectBundleInto"] != nil;

        NSString *delegateName = 
        runningUnitTests ? nil : NSStringFromClass(appDelegateClass);

        @autoreleasepool {
            return UIApplicationMain(argc, argv, nil, delegateName);
        }
    }
< p > @"XCInjectBundleInto" 属性在 environmentDictionary 中是指向你的单元测试包的路径,并由 Xcode 设置。


0
我采用Tomasz Bak的方法,加上dwb答案中的一些代码,得出了以下结果:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{

    BOOL runningTests = NSClassFromString(@"XCTestCase") != nil;
    if (runningTests) {
        self.window.rootViewController = [UIViewController new];
        return true;
    }

    // Your normal code below this
    ....
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接