如何让应用程序知道它是否在纯Swift项目中运行单元测试?


81

在Xcode 6.1中运行测试时,一件令人讨厌的事情是,整个应用程序必须运行并启动其情节提要和根视图控制器。在我的应用程序中,这会运行一些获取API数据的服务器调用。但是,我不希望该应用在运行其测试时执行此操作。

随着预处理器宏的消失,让我的项目知道它是运行测试而不是普通启动时最好的呢?我通常使用command+U并在漫游器上运行它们。

伪代码:

// Appdelegate.swift
if runningTests() {
   return
} else {
   // do ordinary api calls
}

“整个应用程序必须运行并启动其情节提要和根视图控制器”对吗?我没有测试过,但对我来说似乎不合适。嗯...
Fogmeister 2014年

是的,应用程序确实完成了启动以及根视图控制器的viewdidload运行
bogen 2014年

啊,刚刚测试。认为情况并非如此。这是什么导致您的测试出现问题?也许还有另一种解决方法?
Fogmeister 2014年

我只需要考虑一下测试就可以让应用知道运行情况,因此像旧的预处理器宏这样的标志将起作用,但是很快就不支持它们。
bogen 2014年

是的,但是为什么您“需要”这样做呢?是什么让您认为您需要这样做?
Fogmeister 2014年

Answers:


44

无需检查测试是否正在运行以避免产生副作用,您可以在没有主机应用程序本身的情况下运行测试。转到项目设置->选择测试目标->常规->测试->主机应用程序->选择“无”。只需记住包括运行测试所需的所有文件以及Host应用程序目标通常包含的库。

enter image description here


1
删除主机应用程序后如何包括目标的桥接头?
巴尔加夫

尝试过这个问题后,它就折断了我的测试,直到我得到了建议做相反的答案为止:stackoverflow.com/a/30939244/1602270
eagle.dan.1349 '18

如果这样做,我可以测试cloudKit(例如),因此对我来说,解决方案是在applicationDidFinishLaunching中进行检测(如果我正在测试),如果为YES,则在不分配应用程序主类的情况下返回。
user1105951

83

如果您想拥有以前称为纯“逻辑测试”的内容,Elvind的回答还不错。如果您仍然想运行包含的宿主应用程序,但根据是否运行测试有条件地执行或不执行代码,则可以使用以下命令检测是否已插入测试包:

if NSProcessInfo.processInfo().environment["XCTestConfigurationFilePath"] != nil {
     // Code only executes when tests are running
}

我使用了此答案中所述的条件编译标志因此运行时成本仅在调试版本中产生:

#if DEBUG
    if NSProcessInfo.processInfo().environment["XCTestConfigurationFilePath"] != nil {
        // Code only executes when tests are running
    }
#endif

编辑Swift 3.0

if ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] != nil {
    // Code only executes when tests are running
}

3
在最新的Xcode中不起作用-环境变量名称似乎已更改
amleszk,2016年

7
不知道确切什么时候该停止工作,但是对于Xcode 7.3,我现在使用的是XCTestConfigurationFilePath环境密钥,而不是XCInjectBundle
ospr

感谢@ospr,我已经编辑了使用Xcode 7.3的答案
Michael McGuire

@tkuichooseyoupo ProcessInfo.processInfo.environment没有密钥XCTestConfigurationFilePath。您可以分享您的代码吗?再次检查UITest目标
Tal Zion

@TalZion抱歉,没有意识到您要使用UITest目标。您是否尝试过NSClassFromString("XCTest")以下方法?
tkuichooseyou

44

我在application:didFinishLaunchingWithOptions中使用它:

// Return if this is a unit test
if let _ = NSClassFromString("XCTest") {
    return true
}

3
这仅适用于单元测试,不适用于新的Xcode 7 UI测试。
杰西2015年

32

在我看来,其他更简单的方法是:

您可以编辑方案以将布尔值作为启动参数传递给您的应用。像这样:

在Xcode中设置启动参数

所有启动参数都会自动添加到您的中NSUserDefaults

您现在可以像这样获得BOOL:

BOOL test = [[NSUserDefaults standardUserDefaults] boolForKey:@"isTest"];

2
到目前为止,这似乎是我发现的最干净的方法。我们不必将所有应用程序文件都添加到测试目标中,也不必依靠一些奇怪的解决方案来检查"XCTestConfigurationFilePath"NSClassFromString("XCTest")。我已经在Swift中使用全局函数实现了此解决方案func isRunningTests() -> Bool { return UserDefaults.standard.bool(forKey: "isRunningTests") }
Kevin Hirsch

21

我相信知道您是否在测试内运行是完全合法的。有许多原因可能会有所帮助。例如,在运行测试时,我会从App Delegate中的application-did / will-finish-launching方法中提早返回,从而使测试的启动速度更快,而与我的单元测试无关。但是,由于许多其他原因,我不能进行纯粹的“逻辑”测试。

我曾经使用过@Michael McGuire所描述的出色技术。但是,我注意到它在Xcode 6.4 / iOS8.4.1周围不再为我工作(也许它早就崩溃了)。

也就是说,在我的框架的测试目标中运行测试时,我再也看不到XCInjectBundle了。也就是说,我正在测试框架的测试目标内部运行。

因此,利用@Fogmeister建议的方法,我的每个测试方案现在都设置了一个我可以检查的环境变量。

在此处输入图片说明

然后,这是我在称为的类上的一些代码APPSTargetConfiguration,可以为我回答这个简单的问题。

static NSNumber *__isRunningTests;

+ (BOOL)isRunningTests;
{
    if (!__isRunningTests) {
        NSDictionary *environment = [[NSProcessInfo processInfo] environment];
        NSString *isRunningTestsValue = environment[@"APPS_IS_RUNNING_TEST"];
        __isRunningTests = @([isRunningTestsValue isEqualToString:@"YES"]);
    }

    return [__isRunningTests boolValue];
}

使用此方法的一个警告是,如果您从主应用程序方案运行测试(正如XCTest允许的那样)(也就是说,不选择其中一个测试方案),则不会设置此环境变量。


5
而不是将其添加到“运行”中,如果我们将所有方案的“测试”中添加它,是否会更有帮助?这样,isRunningTests将在所有方案中工作。
Vishal Singh'6

@VishalSingh是的,我认为这更干净。您是否尝试过这种方法?让我们知道它是否同样适合您。
idStar 2013年

16
var isRunningTests: Bool {
    return ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] != nil
}

用法

if isRunningTests {
    return "lena.bmp"
}
return "facebook_profile_photo.bmp"

与其他答案不同,此答案有效。谢谢。
n13

8

@Jessy和@Michael McGuire的组合方法

(因为公认的答案对开发框架没有帮助)

所以这是代码:

#if DEBUG
        if (NSClassFromString(@"XCTest") == nil) {
            // Your code that shouldn't run under tests
        }
#else
        // unconditional Release version
#endif

1
这好多了!更短且与框架兼容!
blackjacx

正在开发一个Swift Package,这对我
D. Greg

4

这是我在Swift 4 / Xcode 9中用于单元测试的一种方式。它基于杰西的答案

要完全阻止加载故事板并不容易,但是如果您在didFinishedLaunching的开头添加了故事板,则开发人员可以很清楚地知道发生了什么:

func application(_ application: UIApplication,
                 didFinishLaunchingWithOptions launchOptions:
                 [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
    #if DEBUG
    if let _ = NSClassFromString("XCTest") {
        // If we're running tests, don't launch the main storyboard as
        // it's confusing if that is running fetching content whilst the
        // tests are also doing so.
        let viewController = UIViewController()
        let label = UILabel()
        label.text = "Running tests..."
        label.frame = viewController.view.frame
        label.textAlignment = .center
        label.textColor = .white
        viewController.view.addSubview(label)
        self.window!.rootViewController = viewController
        return true
    }
    #endif

(对于UI测试,您显然不应该这样做,因为您确实希望应用正常启动!)


3

您可以根据此处的方案将运行时参数传递给应用程序...

在此处输入图片说明

但是我想问是否真的需要它。


3

这是快速的方法。

extension Thread {
  var isRunningXCTest: Bool {
    for key in self.threadDictionary.allKeys {
      guard let keyAsString = key as? String else {
        continue
      }

      if keyAsString.split(separator: ".").contains("xctest") {
        return true
      }
    }
    return false
  }
}

这是您的用法:

if Thread.current.isRunningXCTest {
  // test code goes here
} else {
  // other code goes here
}

这是全文: https://medium.com/@theinkedengineer/check-if-app-is-running-unit-tests-the-swift-way-b51fbfd07989


该for循环可以像这样被Swiftilized:return threadDictionary.allKeys.anyMatch { ($0 as? String)?.split(separator: ".").contains("xctest") == true }
Roger Oba

2

其中一些方法不适用于UITests,并且如果您基本上是在使用应用程序代码本身进行测试(而不是将特定代码添加到UITest目标中)。

我最终在测试的setUp方法中设置了一个环境变量:

XCUIApplication *testApp = [[XCUIApplication alloc] init];

// set launch environment variables
NSDictionary *customEnv = [[NSMutableDictionary alloc] init];
[customEnv setValue:@"YES" forKey:@"APPS_IS_RUNNING_TEST"];
testApp.launchEnvironment = customEnv;
[testApp launch];

请注意,这对于我的测试是安全的,因为我目前未使用任何其他launchEnvironment值;如果这样做,您当然要先复制任何现有值。

然后在我的应用程序代码中,如果/当我想在测试期间排除某些功能时,我会寻找此环境变量:

BOOL testing = false;
...
if (! testing) {
    NSDictionary *environment = [[NSProcessInfo processInfo] environment];
    NSString *isRunningTestsValue = environment[@"APPS_IS_RUNNING_TEST"];
    testing = [isRunningTestsValue isEqualToString:@"YES"];
}

注意-感谢RishiG的评论给了我这个主意;我只是将其扩展为一个示例。


1

我一直使用的方法在Xcode 12 beta 1中停止工作。在尝试了所有基于构建的问题解答之后,@ ODB的答案启发了我。这是一个相当简单的解决方案的Swift版本,适用于Real Devices和Simulators。它也应该是相当“发布证明”的。

在测试设置中插入:

let app = XCUIApplication()
app.launchEnvironment.updateValue("YES", forKey: "UITesting")
app.launch()

在应用中插入:

let isTesting: Bool = (ProcessInfo.processInfo.environment["UITesting"] == "YES")

要使用它:

    if isTesting {
        // Only if testing
    } else {
        // Only if not testing
    }

0

为我工作:

目标C

[[NSProcessInfo processInfo].environment[@"DYLD_INSERT_LIBRARIES"] containsString:@"libXCTTargetBootstrapInject"]

迅速: ProcessInfo.processInfo.environment["DYLD_INSERT_LIBRARIES"]?.contains("libXCTTargetBootstrapInject") ?? false


0

首先添加变量进行测试:

在此处输入图片说明

并在您的代码中使用它:

 if ProcessInfo.processInfo.environment["IS_UNIT_TESTING"] == "1" {
                 // Code only executes when tests are running
 } 

0

显然,在Xcode12中,我们需要搜索环境密钥,XCTestBundlePath而不是XCTestConfigurationFilePath如果您使用的是新的XCTestPlan

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.