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

在 Xcode 6.1中运行测试时,一件恼人的事情是,整个应用程序必须运行并启动它的 Storyboard 和 root 视图控制器。在我的应用程序中,它运行一些获取 API 数据的服务器调用。但是,我不希望应用程序在运行测试时这样做。

随着预处理器宏的消失,对于我的项目来说,什么是最好的意识到它是运行测试而不是一个普通的启动?我正常运行他们与 command + U和机器人。

伪代码:

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

您可以根据这里的方案将运行时参数传递到应用程序中..。

enter image description here

但我怀疑这是否真的有必要。

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

enter image description here

如果你想进行过去被称为纯“逻辑测试”的测试,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
}

我相信想知道自己是不是在测试中跑步是完全合理的。有很多理由证明这是有帮助的。例如,在运行测试时,我会从应用程序代理中的 application-did/will-fined- 启动方法提前返回,使得与我的单元测试无关的代码的测试开始得更快。然而,由于许多其他原因,我不能进行纯粹的“逻辑”测试。

我过去常常使用上面@Michael McGuire 描述的优秀技巧。然而,我注意到在 Xcode 6.4/iOS8.4.1(也许它坏得更早)时,这种方法就不再适用于我了。

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

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

enter image description here

然后,这里有一些代码,我有一个类称为 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 允许的那样(即不选择测试模式) ,就不会得到这个环境变量设置。

我在应用程序中使用了这个选项: did FinishLaunchingWithOptions:

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

“ Jessy”和“ Michael McGuire”的组合方式

(在开发框架时,接受的答案不会对您有帮助)

So here is the code:

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

Other, in my opinion simpler way:

你编辑你的方案,将一个布尔值作为启动参数传递给你的应用程序,如下所示:

Set launch arguments in Xcode

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

你现在可以得到这样的 BOOL:

BOOL test = [[NSUserDefaults standardUserDefaults] boolForKey:@"isTest"];
var isRunningTests: Bool {
return ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] != nil
}

用法

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

下面是我在 Swift 4/Xcode 9中用于单元测试的一种方法,它基于 Jesse's answer

阻止故事板被加载并不容易,但是如果你在 did FinishedLaunching 的开头添加了这个,那么你的开发人员就会清楚地知道发生了什么:

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 测试中,你不应该这样做,因为你希望应用程序正常启动!)

Some of these approaches don't work with UITests and if you're basically testing with the app code itself (rather than adding specific code into a UITest target).

最后,我在测试的 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 值; 如果您使用了,那么您当然希望首先复制任何现有的值。

Then in my app code, I look for this environment variable if/when I want to exclude some functionality during a test:

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

注意-感谢 RishiG 的评论,给了我这个想法; 我只是扩展到一个例子。

对我有用:

Objective-C

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

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

更新日期2022

/// Returns true if the thread is running an `XCTest`.
var isRunningXCTest: Bool {
threadDictionary.allKeys
.contains {
($0 as? String)?
.range(of: "XCTest", options: .caseInsensitive) != nil
}
}

原始答案

这是最快的方法。

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

我使用的方法在 Xcode 12 beta 1中停止工作。在尝试了这个问题的所有基于构建的答案之后,我受到了@ODB 答案的启发。下面是一个相当简单的解决方案的 Swift 版本,既适用于真实设备,也适用于模拟器。它也应该是公平的“发布证明”。

Insert in Test setup:

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
}

你现在可以用了

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

在 Xcode 12.5中不推荐使用该选项

首先为测试添加变量:

enter image description here

并在代码中使用它:

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

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

我在我的 SwiftUI Scene.body(Xcode 12.5)中使用这个:

if UserDefaults.standard.value(forKey: "XCTIDEConnectionTimeout") == nil {
// not unit testing
} else {
// unit testing
}

在 Xcode 13.4.1和14.0 b6中,运行和测试之间的区别是:

XCInjectBundleInto = unused
XCTestBundlePath = PlugIns/«Name of Your App»Tests.xctest
XCTestConfigurationFilePath =
XCTestSessionIdentifier = 3B127C55-13E6-4F13-84BD-53FF9FBBC8B2

显然,它不能保证稳定性,但是在两个主要版本之间保持一致性是相当不错的。